var global_decay = 0.99;
var global_threshold = 1;

function rand()
{
    return 1-2*Math.random();
}

//////////////////////////////////////////////////////////////
/// the neuron class
//////////////////////////////////////////////////////////////
function neuron(brain, x, y)
{
    this.brain = brain;
    this.x = x || 0;
    this.y = y || 0;
    
    this.newSpike = 0;
    this.spike = 0;
    this.spikeTime = 0;
    this.value = 0;
    this.weights = [];
    this.inputs = [];
    
    this.decay = global_decay;
    this.threshold = global_threshold;
    
    this.history = [];
}

neuron.prototype.init = function(ibrain, ithreshold, iinputs, iweights, iloss, value, ix, iy, ispikeTime)
{
    with (this)
    {
        //tie the neuron to a brain in a given or default position
        brain = ibrain;
        x = ix || 0;
        y = iy || 0;
        
        //decay and threshold
        decay = global_decay;
        threshold = ithreshold || global_threshold;
        
        //activity
        value = 0 || ivalue;
        spike = (value > threshold) ? 1 : 0;
        newSpike = 0;
        spikeTime = 0 || ispikeTime;
        
        //connections
        weights = iweights || [];
        inputs = [] || iinputs;
        
        //connections sanity check
        if (weights.length != inputs.length)
        {
            weights.length = inputs.length;
            for (var i = 0; i < weights.length; i++)
            {
                if (!weights[i])
                {
                    weights[i] = rand();
                }
            }
        }
        //history is used for logging
        history = [];
    }
}

neuron.prototype.connect = function(neuron, weight)
{
    this.inputs.push(neuron);
    if (weight)
        this.weights.push(weight);
    else
        this.weights.push(rand());
}

neuron.prototype.fire_old = function()
{
    var accumulator = 0;
    for (var i=0; i < this.inputs.length; i++)
    {
        accumulator += this.inputs[i].value * this.weights[i];
    }
    //todo: add bias and threshold...maybe
    this.newValue = 2/(1+Math.exp(-accumulator))-1;
    if (isNaN(this.newValue))
    {
       this.newValue = 0;
    }
}

neuron.prototype.fire = function()
{
    with (this)
    {
	    var v = 0;
	    
	    //mark steps since last spike
	    spikeTime++;
	    if (spike)
	    {
	        value = 0;//should turn negative
	    }
	    else
	    {
	        value *= decay;
	    }
	    
	    //sum all inputs
	    for (var i = 0, len = inputs.length; i < len; i++)
	    {
		    v += inputs[i].newSpike * weights[i];
	    }
		
	    //todo: spike timing dependent add
        value += v;
        history.push(value);
	    
	    //check if needs to fire
	    if (value > threshold)
	    {
	        newSpike = 1;
	        spikeTime = 0;
	    }
	    else
	    {
	        newSpike = 0;
	    }
	    
	    //todo: plasticity
	}
}


//////////////////////////////////////////////////////////////
/// the sensor class
/// extends the neuron but the fire method is actually a callback
//////////////////////////////////////////////////////////////
function sensor(brain, x, y)
{
    this.brain = brain;
    this.x = x;
    this.y = y;
    this.index = 0;
}
sensor.prototype = new neuron();

sensor.prototype.connect = function(){return;}

sensor.prototype.fire = function()
{
    if (this.evalfcn && this.brain.params)
        this.newSpike = parseFloat(this.evalfcn(this.brain.params, this.index));
    else
        this.newSpike = 0;
    if (isNaN(this.newSpike))
    {
        this.newSpike = 0;
    }
}

//////////////////////////////////////////////////////////////
/// the brain class
//////////////////////////////////////////////////////////////

function brain()
{
    this.neurons = new Array();
    this.param = null;
    this.sensorIndexes = new Array();
    this.motorIndexes = new Array();
}

brain.prototype.run = function()
{
    var i;
    with (this)
    {
        var len = neurons.length;
        //calculate all the new values
        for (i = 0; i < len; i++)
        {
            neurons[i].fire();
        }

        //update all the values
        for (i = 0; i < len; i++)
        {
            neurons[i].spike = neurons[i].newSpike;
        }
    }
}

//builds a brain from a genetic code
brain.prototype.fromCode = function(code)
{
    //todo: add individual thresholds and decays
    
    var i = this.sensorIndexes.length; //start from the neurons...for now
    var j = 0;
    var k = 0;

    while(true)
    {
        this.neurons[i].weights[j] = parseFloat(code[k]);
        j++;
        k++;
        if (j >= this.neurons[i].weights.length)
        {
            j = 0;
            i++;
        }
        if (k >= code.length || i >= this.neurons.length)
            break;
    }
}

//saves the brain as genetic code
brain.prototype.toCode = function()
{
    //todo: save individual thresholds and decays as code
    var code = new Array();
    var i = this.sensorIndexes.length; //start from the neurons...for now
    var j = 0;
    var k = 0;

    while(true)
    {
        code.push(parseFloat(this.neurons[i].weights[j].toFixed(5)));
        j++;
        if (j >= this.neurons[i].weights.length)
        {
            j = 0;
            i++;
        }
        if (i >= this.neurons.length)
            break;
    }
    return code.slice();
}

//brain builder...for now as feedforward network
//input: an array of numbers representing the number of cells on each network
//          the first number is the number of sensors and the last one is the number of motors
brain.prototype.buildFF = function()
{
    //start with the sensors
    var nSensors = parseInt(arguments[0]);
    var i,j;
    var prevIndexes = new Array();
    var currentIndexes = new Array();
    for (var kk = 0; kk < nSensors; kk++)
    {
        var s = new sensor(this, 0, 0);
        s.evalfcn = function(params, index)
        {
            //return (isNaN(params[index])) ? 0 : 2/(1+Math.exp(-params[index]));
            return (isNaN(params[index])) ? 0 : params[index];
        }
        currentIndexes[kk] = this.neurons.push(s) - 1;
        this.neurons[currentIndexes[kk]].index = kk;
    }
    this.sensorIndexes = currentIndexes.slice();
    prevIndexes = currentIndexes.slice();
    
    //then for each new number in the inputs, build a new layer and connect it to the previous one
    var nLayers = arguments.length;
    for (j = 1; j < nLayers; j++)
    {
        currentIndexes = [];
        for (i = 0; i < arguments[j]; i++)
        {
            var n = new neuron(this,0, 0);
            for (var k = 0; k < prevIndexes.length; k++)
            {
                n.connect(this.neurons[prevIndexes[k]], rand());
            }
            currentIndexes[i] = this.neurons.push(n) - 1;
        }
        if (j > 1)
        {
            for (i = 0; i < arguments[j-1]; i++)
            {
                var n = this.neurons[prevIndexes[i]];
                for (var k = 0; k < currentIndexes.length; k++)
                {
                    n.connect(this.neurons[currentIndexes[k]], rand());
                }
            }
        }
        prevIndexes = currentIndexes.slice();
    }
    
    //the motors are the last layer
    this.motorIndexes = prevIndexes.slice();
}